// app launch
onCreate called
onStart called
onResume called
// app screen rotated
onResume called
onStop called
onDestroy called
onCreate called
onStart called
onResume called
// app rotated again
onResume called
onStop called
onDestroy called
onCreate called
onStart called
onResume called
public class MainActivity extends AppCompatActivity {
Button button;
LinearLayout layout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
layout = new LinearLayout(this);
layout.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
layout.setLayoutParams(params);
button = new Button(this);
button.setOnClickListener(new OnButtonClicked());
layout.addView(button);
setContentView(layout);
}
private class OnButtonClicked implements View.OnClickListener {
@Override
public void onClick(View v) {
layout.setBackgroundColor(Color.RED);
ColorDrawable[] colors = {new ColorDrawable(Color.RED), new ColorDrawable(Color.WHITE)};
TransitionDrawable transition = new TransitionDrawable(colors);
layout.setBackground(transition);
transition.startTransition(2000);
Log.d("MyTest", "clicked");
}
}
}
Material(
shape: CircleBorder(
side: BorderSide(color: Constants.lineColor, width: Constants.lineWidth),
),
clipBehavior: Clip.hardEdge,
color: Constants.appBackgroundColor,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {},
iconSize: areaHeight*0.8,
color: Constants.playButtonColor,
icon: Icon(
Icons.play_arrow_rounded,
),
)
)
Flutterの最小実装
// This gives below error
// No Directionality widget found. RichText widgets require a Directionality widget ancestor. The specific widget that could not find a Directionality ancestor was:
// RichText [RichText
// The ownership chain for the affected widget is: "RichText <- Text <- [root]"
// Typically, the Directionality widget is introduced by the MaterialApp or WidgetsApp widget at the top of your application widget tree. It determines the ambient reading direction and is used, for example, to determine how to lay out text, how to interpret "start" and "end" values, and to resolve EdgeInsetDirectional, AlignmentDirectional, and other *Directional objects. See also: https://flutter.dev/docs/testing/errors
void main() => runApp(Text("Hello"));
so, below can be a minimum implementation of a Flutter application.
void main() {
runApp(
const Center(
child: Text(
"Hello world",
textDirection: TextDirection.ltr,
),
);
}
In the Flutter documentation:
The runApp() function takes the given Widget and makes it the root of the widget tree. この例ではCenterがroot.
In this example, the widget tree consists of two widgets, the Center widget and its child, the Text widget.
The framework forces the root widget to cover the screen, which means the text “Hello, world” ends up centered on screen. Flutterではroot widgetがscreen全体をcoverするので、”Hello World”はscreenのcenterに来る。
The text direction needs to be specified in this instance; when the MaterialApp widget is used, this is taken care of for you, as demonstrated later. root にCenter widgetを使う場合はtextDirection プロパティを指定する必要があるが、MatrialAppを使う場合は不要。
When writing an app, you’ll commonly author new widgets that are subclasses of either StatelessWidget or StatefulWidget, depending on whether your widget manages any state. 多くの場合StatelessWidgetもしくはStatefulWidget(選択はstateをどう管理するかによる)を継承してカスタムwidgetを作ることになる。
A widget’s main job is to implement a build() function, which describes the widget in terms of other, lower-level widgets. The framework builds those widgets in turn until the process bottoms out in widgets that represent the underlying RenderObject, which computes and describes the geometry of the widget.
in order to activate “hot reload”, embed a widget in the build method
void main() => runApp(MaterialApp(
home: Home(),
));
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Image(
image: AssetImage("assets/girl.jpg"),
),
),
);
}
}
in order to let Flutter know where the assets directory is, write in pubspec.yaml the assets directory location from the root.
assets:
- assets/
function verifyPurchaseAppleStoreKit2(Request $request) {
$sandbox = true;
$transactionID = $request->query("transactionID");
$url = "https://api.storekit.itunes.apple.com/inApps/v1/history/$transactionID";
if ($sandbox) {
$url = "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/history/$transactionID";
}
$handle = curl_init($url);
$token = $this->generateJWTForSigningApplePurchase();
curl_setopt($handle, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token"]);
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
// response includes multiple signedTransactions
$response = curl_exec($handle);
$httpcode = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
// getting a json object from the $response
$json = json_decode($response, false);
return $json;
}
from the above function, you below result:
{
"revision":"1656919939000_2000000096041637",
"bundleId":"com.yourdomain.yourappname.test",
"environment":"Sandbox",
"hasMore":false,
"signedTransactions":[LIST OF SIGNED TRANSACTION]
}
each signedTransaction contains below information.
{
"transactionId":"SOME NUMBER",
"originalTransactionId":"SOME NUMBER",
"bundleId":"com.yourdomain.yourappname.test",
"productId":"com.yourdomain.yourappname.test.in_app_product_name",
"purchaseDate":1644992734000,
"originalPurchaseDate":1644992734000,
"quantity":1,
"type":"Non-Consumable",
"inAppOwnershipType":"PURCHASED",
"signedDate":1658031241903,
"environment":"Sandbox"
}
so, you want to iterate over $json->signedTransactions, and find a match.
foreach($transactions as $transaction) {
}
use Carbon\Carbon;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
public function generateJWTForSigningApplePurchase() {
$signer = new Sha256();
$key = env('JWT_SECRET');
$privateKey = new Key($key);
$time = Carbon::now()->timestamp;
$token = (new Builder())
->issuedBy(env('ISSUER_ID'))
->issuedAt($time)
->expiresAt($time + (60 * 60))
->withClaim('bid', env('BUNDLE_ID'))
->withClaim('aud', "appstoreconnect-v1")
->withHeader('alg', 'ES256')
->withHeader('kid', env('KID'))
->withHeader('typ', 'JWT')
->getToken($signer, $privateKey);
return $token->__toString();
}
Client-side code in Swift
func verify(id: String) {
let urlStringVerifyPurchase = "https://YOUR_DOMAIN.COM/verify/"
Task {
guard let verificationResult = await products.filter({ $0.id == id}).first?.currentEntitlement else { return }
switch verificationResult {
case .verified(let transaction):
// your validation function
validation(transaction: transaction)
let queryItems = [URLQueryItem(name: "transactionID", value: "\(transaction.originalID)")]
var urlComps = URLComponents(string: urlStringVerifyPurchase)!
urlComps.queryItems = queryItems
guard let url = urlComps.url else { return }
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let data = data else {
print("no data")
return
}
do {
let decode = try JSONDecoder().decode(ServerVerifyPurchaseResponse.self, from: data)
if decode.code == 200 {
self?.performDownload(id: id)
}
} catch {
print("error occurred when decoding verify purchase data: \(error)")
}
}
task.resume()
case .unverified(let transaction, _):
print(transaction)
}
}
}
@State var isLongPressed = false
var longPress: some Gesture {
LongPressGesture(minimumDuration: 3)
.onEnded { ges in
self.isLongPressed = true
}
}
ProductリストをUIに表示させるバックエンドフローは以下
// array of fetched product ids
var identifiers: [String] = []
// array of Product
var products: [Products] = []
// fetch from server and store data in Keychain
func fetchProductsFromServer() async {
let url = URL(string: "https://jamapp.me/test_getInAppProducts")!
do {
let (data, _) = try await URLSession.shared.data(from: url)
KeychainWrapper.standard.set(data, forKey: dataKey)
} catch {
print(error)
}
}
// decode from keychain and fill identifier array
func decodeProductsFromKeychain() async {
guard let data = KeychainWrapper.standard.data(forKey: self.dataKey) else { return }
do {
let decode = try JSONDecoder().decode(ServerResponse.self, from: data)
decodedProducts = Set(decode.products)
for product in decode.products {
identifiers.insert(product.identifier)
}
} catch {
print(error)
}
}
// fetch from AppStore
func fetchProductsFromAppStore() async {
do {
products = try await Product.products(for: identifiers)
for product in products {
if await product.currentEntitlement != nil {
// storing currentEntitlement in Keychain for off-line use...
KeychainWrapper.standard.set(true, forKey: product.id)
// storing in isPurchasedDict to update UI
await MainActor.run {
self.isPurchasedDict[product.id] = KeychainWrapper.standard.bool(forKey: product.id)
}
} else {
KeychainWrapper.standard.set(false, forKey: product.id)
await MainActor.run {
self.isPurchasedDict[product.id] = KeychainWrapper.standard.bool(forKey: product.id)
}
}
}
} catch {
print(error)
}
}
in order to handle any unhandled transactions, listen for Transaction.updates in appDelegate
extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// listening for transaction updates
Task {
await listenForTransactions()
}
return true
}
func listenForTransactions() async {
for await verificationResult in Transaction.updates {
switch verificationResult {
case .verified(let transaction):
KeychainWrapper.standard.set(true, forKey: transaction.productID)
await transaction.finish()
case .unverified(let transaction, _):
await transaction.finish()
}
}
}
}
in order to buy a product, do below.
func buyProduct(product: Product) {
Task {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await updatePurchase(transaction: transaction)
await transaction.finish()
case .unverified(let transaction, _):
await transaction.finish()
}
case .userCancelled:
()
case .pending:
()
@unknown default:
()
}
} catch {
print(error)
}
}
}
func updatePurchase(transaction: Transaction) async {
KeychainWrapper.standard.set(true, forKey: transaction.productID)
await MainActor.run {
isPurchasedDict[transaction.productID] = true
}
}
Title Barを消すにはres -> values -> themes -> themes.xml 内の下記を変更する
<style name="Theme.MidiPractice" parent="Theme.MaterialComponents.DayNight.NoActionBar">
もしくは、ActivityのonCreate()で以下を実行する
getSupportActionBar().hide();
StatusBarの色を変更するには res -> values -> themes -> themes.xml 内の下記を変更する
<item name="android:statusBarColor" tools:targetApi="l">@color/purple_200</item>
StatusBarのBackgroundColorを白にする場合は以下のように変更
<item name="android:statusBarColor" tools:targetApi="l">@color/white</item>
<item name="android:windowLightStatusBar">true</item>
private int getScreenWidth() {
return Resources.getSystem().getDisplayMetrics().widthPixels;
}
private int getScreenHeight() {
return Resources.getSystem().getDisplayMetrics().heightPixels;
}
UIScrollViewをスクロールさせるためには、scrollViewにSubviewとしてもう一つUIViewをcontentsViewとして追加し、contentsView.frame.sizeをscrollView.contentSizeに教える必要があります。
import UIKit
class ViewController: UIViewController {
// MARK: - UI Components
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
return scrollView
}()
lazy var contentsView: UIView = {
let contentsView = UIView()
return contentsView
}()
var labels: [UILabel] = []
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
addViews()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setupLayouts()
}
private func addViews() {
view.addSubview(scrollView)
scrollView.addSubview(contentsView)
}
// MARK: - Layouts
private func setupLayouts() {
// remove and clear labels
labels.forEach { label in
label.removeFromSuperview()
}
labels = []
// constraining scrollView to the view
scrollView.frame = CGRect(x: 0, y: 0, width: view.bounds.width/2, height: view.bounds.height/2)
scrollView.backgroundColor = .white
// to only allow vertical scroll, constrain the contentsView width to the scrollView width
contentsView.frame = CGRect(x: 0, y: 0, width: scrollView.bounds.width, height: 0)
// to get aprox. same fontSize when the screen rotates
var fontSize: CGFloat = 0
if view.bounds.width > view.bounds.height {
fontSize = view.bounds.height*0.5*0.2
} else {
fontSize = view.bounds.width*0.5*0.2
}
// adding labels to the contentsView
for i in 0..<50 {
let label = UILabel()
label.frame = CGRect(x: 0, y: CGFloat(i)*fontSize, width: contentsView.bounds.width, height: fontSize)
label.font = UIFont(name: "HelveticaNeue-CondensedBold", size: fontSize)
label.text = "Label \(i)"
label.textColor = .black
contentsView.addSubview(label)
// retaining lables for removing from its superView for when screen rotates, etc...
labels.append(label)
}
// adjusting the contentsView height to the number of lables added
contentsView.frame.size = CGSize(width: scrollView.bounds.width, height: fontSize*50)
// to tell the scrollView the frame.size of the contentsView is important, otherwise will not scroll
scrollView.contentSize = contentsView.frame.size
}
}